Skip to content

fix(dashboard): clear the CopyButton reset timer#528

Merged
NiveditJain merged 3 commits into
FailproofAI:mainfrom
Nitjsefnie-OSC:fix/523-copy-button-timer
Jul 20, 2026
Merged

fix(dashboard): clear the CopyButton reset timer#528
NiveditJain merged 3 commits into
FailproofAI:mainfrom
Nitjsefnie-OSC:fix/523-copy-button-timer

Conversation

@Nitjsefnie

@Nitjsefnie Nitjsefnie commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What

Fixes both defects reported in #523 for CopyButton (app/components/copy-button.tsx), where the "copied ✓" revert timer was fired with no stored id and no cleanup:

  1. Rapid re-click — clicking Copy again before the previous 2s window elapsed left the stale timer from the first click still armed, so it could flip the checkmark back to the copy icon early (0.5s ahead of schedule in the issue's example), contradicting the feedback from the just-completed second click.

    Fixed by storing the timer id in a useRef and clearTimeout-ing it before arming a new one on every copy, so only the latest click's timer controls the revert.

  2. Unmount mid-window — if the component unmounted (e.g. the sessions table re-paginating/filtering a row away) before the 2s elapsed, setCopied still ran, producing a React "state update on an unmounted component" warning.

    Fixed by clearing the same ref's timer in a useEffect cleanup (return () => clearTimeout(...), empty deps) that runs on unmount.

This follows the exact idiom already used elsewhere in app/components/ for ref-tracked, re-armable timers (e.g. raw-log-viewer.tsx's highlightTimerRef), rather than introducing a new pattern.

Bonus (also called out in the issue): added the missing type="button" to the <button> so it can't act as an implicit form submit.

What's unchanged

  • Same 2s revert window.
  • Same copy/check icons and same conditions for showing each.
  • Same component props/API (text, className) — no new props, no new dependency.
  • Same clipboard-write / fallback / error-handling behavior.

Tests

__tests__/components/copy-button.test.tsx already existed with component-test infrastructure (Vitest + Testing Library + fake timers), so I extended it rather than inventing new infra:

  • Rapid re-click: click at t=0, click again at t=1.5s, assert the checkmark survives past the first click's original t=2.0s deadline, then reverts at t=3.5s (2s after the last click) — confirms the stale timer no longer cuts the feedback short.
  • Unmount mid-window: click, advance to t=0.5s, unmount, then advance well past the original t=2.0s deadline and assert console.error was never called (no state-update-on-unmounted-component warning).

All existing tests in the file continue to pass unmodified.

Gates (CONTRIBUTING.md)

bun run lint && bunx tsc --noEmit && bun run test:run && bun run build
  • bun run lint — 0 errors (5 pre-existing warnings in unrelated files: <img>/no-img-element and one unused eslint-disable, none touched by this change).
  • bunx tsc --noEmit — clean, no output.
  • bun run test:run — 2065 passed, 1 pre-existing failure in __tests__/hooks/integrations.test.ts (writeHookEntries adds a packages-array entry to a fresh settings.json) unrelated to this change — that test asserts the literal substring "failproofai" appears in a path derived from the checkout directory name, and fails identically on unmodified main in this same checkout (verified via git stash).
  • bun run build — compiles successfully, TypeScript passes, all routes generate.

Closes #523

Prepared with AI assistance (Claude Sonnet 5), human-reviewed before submission.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed the dashboard CopyButton so the “copied” check remains visible for the full 2 seconds after rapid repeated clicks, without the earlier timer resetting it early.
    • Ensured any pending revert timer is cleared on unmount to prevent delayed state updates and related console warnings (e.g., when rows are filtered mid-window).
    • The 2-second confirmation now starts only after a successful copy.
  • Tests
    • Added fake-timer tests covering rapid re-click behavior and unmount safety (no pending timers and no post-unmount console errors).

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

CopyButton now stores and cleans up its copied-state revert timer, re-arms the two-second window after successful copies, and explicitly declares its button type. Tests cover rapid clicks and unmounting during the pending timer.

Changes

CopyButton timer lifecycle

Layer / File(s) Summary
Timer lifecycle and copy integration
app/components/copy-button.tsx
The component stores its revert timeout in a ref, clears it before rearming and on unmount, arms it after successful clipboard or fallback copies, and sets type="button".
Timer behavior validation
__tests__/components/copy-button.test.tsx, CHANGELOG.md
Tests cover the full two-second window after rapid clicks and timer advancement after unmount; the changelog records the release fix.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Poem

A bunny clicks twice, the checkmark stays bright,
No stale timer steals its spotlight.
When rows hop away, cleanup takes flight,
Two seconds remain just right.
“Copy!” says the rabbit, “What a delight!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: clearing the CopyButton reset timer.
Description check ✅ Passed It clearly explains the bug, fix, tests, and verification, though it doesn't use the template's exact headings.
Linked Issues check ✅ Passed The code addresses #523 by storing, re-arming, and cleaning the timer, adding type="button", and covering both regressions with tests.
Out of Scope Changes check ✅ Passed No unrelated changes stand out; the changelog entry, tests, and button/type cleanup all support the linked CopyButton fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Automated code review started - full review. Results will be posted here.

@Nitjsefnie
Nitjsefnie force-pushed the fix/523-copy-button-timer branch from 55b9f9b to c09de15 Compare July 16, 2026 11:53
@hermes-exosphere

Copy link
Copy Markdown
Contributor

Review in progress — Phase 0 complete.

Existing comments: 2 issue comments (1 from coderabbitai, 1 from hermes-exosphere). No review threads exist.
Focused review on commit c09de15 — the CopyButton timer fix.

Now analyzing code and running tests...

Comment thread app/components/copy-button.tsx
@hermes-exosphere

Copy link
Copy Markdown
Contributor

🔍 Automated Code Review

📋 Executive Summary

This PR fixes two real bugs in the CopyButton component (rapid re-click timer corruption and unmount-after-copy state updates) by storing the timeout ID in a useRef and clearing it on re-arm and unmount. The fix is clean, correctly follows the established pattern from raw-log-viewer.tsx, and includes good tests. One issue found: the PR description and commit message claim type="button" was added, but it's actually missing from the code.


📊 Change Architecture

graph TD
    A["CopyButton Component<br/>copy-button.tsx"] -->|"fixed: ref-track timer + cleanup"| B["armRevertTimer()<br/>useCallback"]
    B -->|"clearTimeout before re-arm"| C["Rapid Re-click Fix<br/>✅ Stale timer cleared"]
    C --> D["Revert Timer<br/>setTimeout(setCopied(false), 2000)"]
    A -->|"useEffect cleanup on unmount"| E["Unmount Safety<br/>✅ Timer cleared"]
    E --> F["No React Warning"]
    G["Tests<br/>copy-button.test.tsx"] -->|"+61 lines, 2 new tests"| C
    G -->|"+61 lines, 2 new tests"| E
    H["Missing: type=button"] -.->|"⚠️ Claimed but not implemented"| A
    style A fill:#87CEEB
    style C fill:#90EE90
    style E fill:#90EE90
    style H fill:#FF6347
    style G fill:#90EE90
Loading

Legend: 🟢 New/Fixed | 🔵 Modified | 🔴 Issue Found


🔴 Breaking Changes

✅ No breaking changes detected. The component's public API (text, className) and behavior (2s revert window, icons, clipboard API) are unchanged.


⚠️ Issues Found

  1. 🟡 Inconsistencyapp/components/copy-button.tsx:68type="button" is claimed in both the PR description (line: "added the missing type=\"button\"") and commit message body (line: "Also add the missing type=\"button\""), but the <button> element on line 68 does NOT have type="button". The linked issue Dashboard CopyButton: the "copied ✓" timer is never cleared #523 also explicitly called this out: "Bonus: the <button> is also missing type=\"button\"". This is either a forgotten implementation or a misleading commit/PR description. [See inline comment]

🔬 Logical / Bug Analysis

Copy Button (app/components/copy-button.tsx):

Check Result Notes
Race condition (rapid re-click) ✅ Fixed clearTimeout(revertTimerRef.current) before setting new timeout
Unmounted state update ✅ Fixed useEffect cleanup clears timer on unmount
Ref initialization ✅ Correct useRef<ReturnType<typeof setTimeout>>(undefined) — proper typing
armRevertTimer dependency ✅ Correct useCallback([], []) — stable reference, no re-creation
handleCopy dependency ✅ Correct Added armRevertTimer to dep array — no stale closure
Both clipboard paths updated ✅ Both paths Both navigator.clipboard.writeText success AND fallbackCopyText fallback call armRevertTimer()
Error path (both fail) ✅ No timer armed Catch block does NOT call armRevertTimer() when both methods fail
Pattern consistency ✅ Matches raw-log-viewer.tsx Same Ref + clearTimeout + useEffect cleanup pattern

No logical errors, race conditions, or bugs detected in the implementation.

Tests (__tests__/components/copy-button.test.tsx):

Test Coverage Quality
"reverts to copy icon after 2s" (existing) Happy path, single click ✅ Good
"keeps checkmark after rapid clicks" (NEW) Edge: rapid re-click at t=1.5s ✅ Excellent — asserts at t=2.0s (stale timer prevented) AND t=3.5s (new timer fires correctly)
"does not update state after unmount" (NEW) Edge: unmount mid-window ✅ Good — spies on console.error, unmounts at t=0.5s, advances past 2s, asserts no warning
"falls back to execCommand" (existing) Error: clipboard unavailable ✅ Good
"does not show check when both fail" (existing) Error: both clipboard methods fail ✅ Good
"renders copy icon by default" (existing) Initial state ✅ Good
"copies text and shows check" (existing) Happy path ✅ Good
"applies custom className" (existing) Props passthrough ✅ Good

Edge case analysis for the unmount test:

  • The test uses vi.spyOn(console, "error") and asserts .not.toHaveBeenCalled(). This is a valid test — if the component's setCopied(false) fires after unmount, React calls console.error with the "state update on unmounted component" warning.
  • Correct approach — the spy catches any console.error call. The test would fail if console.error was called for any reason, which is appropriate since no code in the test path should produce errors.

🧪 Evidence — Build & Test Results

TypeScript: ✅ Clean — bunx tsc --noEmit — no errors

Lint: ✅ Clean — bun run lint — 0 errors (5 pre-existing warnings unrelated to this change: 4 no-img-element, 1 unused eslint-disable directive)

Unit Tests — CopyButton only:

✓ __tests__/components/copy-button.test.tsx (8 tests) 469ms

All 8 tests pass (6 existing + 2 new)

Full Test Suite:

 Test Files  121 passed (121)
      Tests  2066 passed (2066)
   Duration  66.21s

✅ All 2066 tests pass across 121 test files. Zero failures. No regressions.

Build:

✅ TypeScript compilation passes (bunx tsc --noEmit)
✅ Lint passes (0 errors)

🔗 Issue Linkage

This PR closes #523 — "Dashboard CopyButton: the 'copied ✓' timer is never cleared". The issue described both defects (rapid re-click and unmount mid-window) with reproduction steps. The implementation directly addresses both bugs using the exact approach suggested in the issue ("Store the timeout id in a useRef, clearTimeout it before re-arming, and clear it in a useEffect(() => () => clearTimeout(ref.current), []) cleanup").

One discrepancy: The issue also noted "Bonus: the <button> is also missing type=\"button\" at :53" — the PR claims to have added this but the code does NOT include it.


👥 Human Review Feedback

No human review comments exist on this PR. Two bot comments:

  • coderabbitai[bot]: Auto-generated review stack link — informational only
  • hermes-exosphere: "Automated code review started" — prior review start marker

No unresolved review threads.


💡 Suggestions

  1. Add type="button": This was documented as part of the fix — it should be added to prevent implicit form submission. Single-line addition:

    <button
      type="button"    // ← add this
      onClick={handleCopy}
  2. Consider extracting the magic number 2000: The 2-second revert timeout is used in both armRevertTimer and tests. Extracting it to a named constant (e.g., const REVERT_DELAY_MS = 2000) at the module level would improve maintainability and testability:

    const REVERT_DELAY_MS = 2000;
  3. Consider adding a test for rapid clicks in the fallback path: The existing tests for clipboard unavailable (execCommand) don't test rapid re-click. Since armRevertTimer() is called in all paths (clipboard API AND fallback), this could be a gap. However, the fallback is synchronous while the API path is async, so the timer behavior is consistent — low priority.


🏆 Verdict

VERDICT: APPROVED_WITH_SUGGESTIONS

The core fix is correct, well-tested, and follows existing patterns. All tests pass, TypeScript is clean, lint is clean. One blocking issue: type="button" must be added as documented (or the PR description updated if it was intentionally omitted).


Automated code review · 2026-07-16 12:00:50 UTC

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: Approved with suggestions (see summary + inline comment). Core fix is correct and well-tested. One issue: type="button" is claimed in description/commit but missing from code. ✅

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: Approved. ✅

@Nitjsefnie

Copy link
Copy Markdown
Contributor Author

Good catch on the mismatch — that was my doing, and the resolution is to restore the code rather than edit the description.

I'd stripped type="button" on a reflex about CONTRIBUTING's "one concern per PR", without re-reading the issue closely enough. #523 explicitly asks for it: "(Bonus: the <button> is also missing type="button" at :53.)" — so it's part of this issue's stated scope, not a drive-by. It's back in, and the code, commit message and description now agree.

Verified after restoring: tsc --noEmit clean, copy-button.test.tsx 8/8 passing.

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Automated code review started - full review. Results will be posted here.

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Re-review in progress — Phase 0-2 complete. Previously reported issue has been resolved. Running build & tests now...

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Resolved 1 previously-reported issue — the type="button" attribute has been added (line 69 in the updated commit). The code, commit message, and PR description now agree.

@hermes-exosphere

Copy link
Copy Markdown
Contributor

🔍 Automated Code Review (Re-review)

📋 Executive Summary

This PR fixes two real bugs in the CopyButton component: rapid re-click timer corruption and unmount-after-copy state-update warnings. The fix stores the timeout ID in a useRef, clears it on re-arm and unmount, and follows the identical pattern already used in raw-log-viewer.tsx. Two well-written tests verify the race condition and the unmount safety. The previously reported type="button" issue has been resolved — the attribute is now present on line 69, and the code, commit message, and PR description all agree. Code quality is high: clean TypeScript, clean lint, all 2,066 tests pass.


📊 Change Architecture

graph TD
    A["CopyButton<br/>copy-button.tsx<br/>(modified)"] -->|"ref-track timer + cleanup"| B["armRevertTimer()<br/>useCallback"]
    B -->|"clearTimeout before re-arm"| C["Rapid Re-click Fix<br/>Stale timer cleared"]
    C --> D["Revert Timer<br/>setTimeout(setCopied(false), 2000)"]
    A -->|"useEffect cleanup on unmount"| E["Unmount Safety<br/>Timer cleared"]
    E --> F["No React Warning"]
    G["Tests<br/>copy-button.test.tsx<br/>(+61 lines)"] -->|"2 new tests"| C
    G -->|"2 new tests"| E
    H["type='button'<br/>Added"] -.-> A
    style A fill:#87CEEB
    style C fill:#90EE90
    style E fill:#90EE90
    style H fill:#90EE90
    style G fill:#90EE90
Loading

Legend: Green = New/Fixed | Blue = Modified


Breaking Changes

No breaking changes detected. The component's public API (text, className) and behavior (2s revert window, icons, clipboard API) are unchanged.


Issues Found

No issues found. All previously reported issues have been addressed:

  1. Resolvedapp/components/copy-button.tsx:69type="button" is now present. The button can no longer act as an implicit form submit.

Logical / Bug Analysis

Copy Button (app/components/copy-button.tsx):

Check Result Notes
Race condition (rapid re-click) Fixed clearTimeout(revertTimerRef.current) before setting new timeout
Unmounted state update Fixed useEffect cleanup clears timer on unmount
Ref initialization Correct useRef<ReturnType<typeof setTimeout>>(undefined) — proper typing
armRevertTimer dependency Correct useCallback([], []) — stable reference, no re-creation
handleCopy dependency Correct Added armRevertTimer to dep array — no stale closure
Both clipboard paths updated Both paths Both navigator.clipboard.writeText success AND fallbackCopyText fallback call armRevertTimer()
Error path (both fail) No timer armed Catch block does NOT call armRevertTimer() when both methods fail
type="button" Present Line 69 — prevents implicit form submission
Pattern consistency Matches raw-log-viewer.tsx Same Ref + clearTimeout + useEffect cleanup pattern

No logical errors, race conditions, or bugs detected in the implementation.

Tests (__tests__/components/copy-button.test.tsx):

Test Coverage Quality
"reverts to copy icon after 2s" (existing) Happy path, single click Good
"keeps checkmark after rapid clicks" (NEW) Edge: rapid re-click at t=1.5s Excellent — asserts at t=2.0s (stale timer prevented) AND t=3.5s (new timer fires correctly)
"does not update state after unmount" (NEW) Edge: unmount mid-window Good — spies on console.error, unmounts at t=0.5s, advances past 2s, asserts no warning
"falls back to execCommand" (existing) Error: clipboard unavailable Good
"does not show check when both fail" (existing) Error: both clipboard methods fail Good
"renders copy icon by default" (existing) Initial state Good
"copies text and shows check" (existing) Happy path Good
"applies custom className" (existing) Props passthrough Good

Evidence — Build & Test Results

TypeScript: Clean — bunx tsc --noEmit produces no output (zero errors).

Lint: 0 errors (5 pre-existing warnings in unrelated files — 4 no-img-element, 1 unused eslint-disable directive).

CopyButton Tests — 8/8 passed:

  CopyButton > renders copy icon by default
  CopyButton > copies text to clipboard on click and shows check icon
  CopyButton > reverts to copy icon after 2 seconds
  CopyButton > keeps the checkmark for the full 2s window after the last of two rapid clicks
  CopyButton > does not update state or warn after unmounting mid-window
  CopyButton > applies custom className
  CopyButton > falls back to execCommand when navigator.clipboard is unavailable
  CopyButton > does not show check icon when both copy methods fail

Full Test Suite — 2,066/2,066 passed:

 Test Files  121 passed (121)
      Tests  2066 passed (2066)
   Duration  107.46s

Build — Successful:

bun run build — TypeScript passes, all routes generate.
[prune-standalone] 2287 files / 65.32 MB -> 1521 files / 49.19 MB

Issue Linkage

This PR closes #523 — "Dashboard CopyButton: the 'copied checked' timer is never cleared". Both defects (rapid re-click and unmount mid-window) are addressed exactly as described in the issue. The "Bonus: the <button> is also missing type="button"" from #523 has also been added.


Human Review Feedback

  • @Nitjsefnie (PR author): "Good catch on the mismatch — that was my doing... It's back in..." — The author acknowledged the type="button" issue from the previous review and restored it. Addressed.

No other human review feedback exists on this PR.


Suggestions

  1. Consider extracting the magic number 2000: The 2-second revert timeout is used in both armRevertTimer and the tests. Extracting it to a named constant (const REVERT_DELAY_MS = 2000) at the module level would improve maintainability and make tests more self-documenting.

  2. Consider adding a test for rapid clicks in the fallback path: The existing tests for clipboard unavailable (execCommand) don't test rapid re-click. Since armRevertTimer() is called in all paths (clipboard API AND fallback), the timer behavior is consistent — low priority but would complete the coverage.


Verdict

VERDICT: APPROVED

All previously reported issues are resolved. The implementation is correct, well-tested, follows existing patterns (raw-log-viewer.tsx), and all gates pass (TypeScript, lint, all 2,066 tests, build).


Automated code review · 2026-07-16 17:55 UTC

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review (re-review): All previous issues resolved. TypeScript clean, lint clean, all 2,066 tests pass, build succeeds. VERDICT: APPROVED.

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: Approved. ✅

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Your PR is awaiting review by a moderator. Till then you can join the Discord for conversation: https://discord.gg/qaFM2uYFb

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Your PR is awaiting review by a moderator. Till then you can join the Discord for conversation: https://discord.befailproof.ai

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Your PR is awaiting review by a reviewer. Till then you can join the Discord for conversation: https://discord.befailproof.ai

@chhhee10

Copy link
Copy Markdown
Contributor

Went through the timer paths — a re-click resets the window properly and the unmount cleanup does what it says on the tin. Tests are a nice touch, and useRef<...>(undefined) is the right call under the React 19 types.

Same note as elsewhere: needs a CHANGELOG.md entry. That rule is stuck in CLAUDE.md instead of CONTRIBUTING.md so it's our fault it isn't discoverable — add a line and I'm happy.

Nitjsefnie added a commit to Nitjsefnie-OSC/failproofai that referenced this pull request Jul 17, 2026
Per review: the CHANGELOG-per-PR rule in CLAUDE.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Nitjsefnie

Copy link
Copy Markdown
Contributor Author

Thanks for the careful timer-path walkthrough! CHANGELOG entry added (13164d8) under a new 0.0.14-beta.1 — 2026-07-17 section per the CLAUDE.md rule — and no worries about discoverability, it was a one-liner once pointed at.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@__tests__/components/copy-button.test.tsx`:
- Around line 103-127: Update the “does not update state or warn after
unmounting mid-window” test to spy on clearTimeout and assert it is called
during unmount, replacing the ineffective console.error assertion. Remove the
console.error spy and preserve the existing timer advancement and CopyButton
interaction flow.

In `@app/components/copy-button.tsx`:
- Line 28: Update the revertTimerRef useRef declaration to include undefined in
its generic type, preserving the current undefined initialization and timeout
return type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 164b567b-1355-4f92-893d-9067c3af6903

📥 Commits

Reviewing files that changed from the base of the PR and between 446484b and 13164d8.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • __tests__/components/copy-button.test.tsx
  • app/components/copy-button.tsx

Comment thread __tests__/components/copy-button.test.tsx Outdated
Comment thread app/components/copy-button.tsx
@Nitjsefnie

Copy link
Copy Markdown
Contributor Author

Both CodeRabbit comments evaluated against the code rather than taken on faith:

Comment 1 (vacuous unmount test): correct — confirmed empirically and fixed in d17dc3d. With the useEffect cleanup deliberately removed, the old test still passed — React 18+ indeed no longer emits the unmounted-setState warning, so errorSpy was load-bearing on nothing. The test is now clears the armed revert timer on unmount: it asserts vi.getTimerCount() is 1 mid-window and 0 after unmount (probed the timer count at every stage first to rule out userEvent's own timers polluting it — clean). Mutation-checked: removing the cleanup now fails on exactly that assertion; restored, 8/8. The errorSpy check stays as an explicitly-secondary belt-and-suspenders line.

Comment 2 (useRef TS error): refuted. bunx tsc --noEmit exits 0 on a fresh run of this exact branch (and this repo's CI typecheck has been green on it throughout) — under the React 19 types, useRef<ReturnType<typeof setTimeout>>(undefined) resolves fine via the T | undefined overload. Left untouched.

Full gates re-run on the new head: tsc clean, bun run test:run 2066 passed, build green, lint clean (15s this run).

Nitjsefnie added a commit to Nitjsefnie-OSC/failproofai that referenced this pull request Jul 17, 2026
Per review: the CHANGELOG-per-PR rule in CLAUDE.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Nitjsefnie
Nitjsefnie force-pushed the fix/523-copy-button-timer branch from d17dc3d to da72a9e Compare July 17, 2026 12:35
@Nitjsefnie

Copy link
Copy Markdown
Contributor Author

Rebased onto current main — this needed a force-push, so unfortunately your approval got dismissed along with it. Sorry for the re-review tax.

Cause: main picked up its own ## 0.0.14-beta.1 — 2026-07-17 CHANGELOG heading this morning (#563/#564/#567), and this branch had added that same heading independently, so the two collided at the top of the file. Resolved by dropping our duplicate heading and folding the entry into the existing section's ### Fixes list instead — the changelog text itself is unchanged.

No source changes in the rebase. Re-verified on the new head: bun run lint clean, bunx tsc --noEmit clean, bun run test:run green, bun run build green. The regression tests were re-mutation-checked against upstream/main's version of the production file (they fail there, pass on this branch).

Nitjsefnie added a commit to Nitjsefnie-OSC/failproofai that referenced this pull request Jul 17, 2026
Per review: the CHANGELOG-per-PR rule in CLAUDE.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Nitjsefnie
Nitjsefnie force-pushed the fix/523-copy-button-timer branch from da72a9e to 7c04e77 Compare July 17, 2026 16:42
@Nitjsefnie

Copy link
Copy Markdown
Contributor Author

Rebased onto current main again — the top of the ### Fixes list under ## 0.0.14-beta.1 — 2026-07-17 had picked up two more entries (#560) since the last rebase, colliding with this branch's #528 entry. Resolved the same way as before: kept your new entries and folded ours back into the existing ### Fixes list, with the changelog text itself unchanged. No source changes in the rebase.

Re-verified on the new head: bun run lint clean, bunx tsc --noEmit clean, bun run test:run green (2203 passed), bun run build green. The unmount regression test was re-mutation-checked against upstream/main's app/components/copy-button.tsx (it fails there — getTimerCount() returns 1 instead of 0 without the useEffect cleanup — and passes on this branch). This needed a force-push, so apologies that it dismisses the approval once more.

(Disclosure: this rebase and verification were done with AI assistance.)

@chhhee10

Copy link
Copy Markdown
Contributor

Thanks for this — the fix itself is good. Reviewed by running it, and it holds up:

One thing before this can merge: it now conflicts with main on CHANGELOG.md. #570 landed a sibling entry at the top of the same ### Fixes block, so your entry and that one collide. It's a purely additive conflict — both lines just need to be kept.

Could you rebase onto latest main? Nothing else needs to change; I verified the resolution locally and your app/components/copy-button.tsx and __tests__/components/copy-button.test.tsx come through byte-identical.

Also worth noting: the required CI jobs (quality / test / build / test-e2e) never ran on this PR — only CodeRabbit and Socket did. A rebase push should kick them off properly.

Nitjsefnie and others added 3 commits July 20, 2026 07:40
The revert-to-copy-icon setTimeout was fired on every click with no
stored id and no cleanup, causing two bugs:

- Rapid re-click: clicking again before the 2s window elapsed left the
  stale timer from the first click running, so it could flip the
  checkmark back to the copy icon early, contradicting the just-shown
  feedback from the second click.
- Unmount mid-window: if the component unmounted (e.g. sessions table
  re-paginating/filtering) before the timer fired, setCopied ran on an
  unmounted component, logging a React warning.

Store the timer id in a ref, clearTimeout it before re-arming on each
copy, and clear it in a useEffect cleanup on unmount. Also add the
missing type="button" the issue called out, so the button can't act as
an implicit form submit.

Closes FailproofAI#523

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per review: the CHANGELOG-per-PR rule in CLAUDE.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prior assertion (errorSpy not called) is vacuous under React 18+,
which dropped the state-update-on-unmounted-component warning: it passed
even with the useEffect cleanup removed. Replace it with a load-bearing
vi.getTimerCount() check — the armed 2s revert timer must be pending
mid-window (1) and cleared to 0 by the unmount cleanup. The errorSpy
assertion is kept as a secondary belt-and-suspenders check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Nitjsefnie
Nitjsefnie force-pushed the fix/523-copy-button-timer branch from 7c04e77 to 72f3451 Compare July 20, 2026 07:44
@Nitjsefnie

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main (72f3451). The only conflict was the additive CHANGELOG.md one you flagged — #570's entry and this PR's (#528) entry now sit together in the ### Fixes list, both kept, nothing else reordered or dropped. app/components/copy-button.tsx and __tests__/components/copy-button.test.tsx come through byte-identical (confirmed by diff against the pre-rebase tip — the rebase touched only CHANGELOG.md).

Re-verified locally: bun run lint (0 errors), bunx tsc --noEmit (clean), bun run build (green), bun run test:run (2236 passed, 0 failed). The force-push should kick off the required CI jobs (quality / test / build / test-e2e) now. Thanks for running it locally to confirm the resolution.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
__tests__/components/copy-button.test.tsx (1)

103-132: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove ineffective console.error spy.

React 18+ removed the "state update on unmounted component" warning. Since this test now robustly verifies the unmount cleanup via vi.getTimerCount(), the errorSpy and the subsequent timer advancement block are dead code testing a non-existent React warning. Removing them will simplify the test.

♻️ Proposed refactor
-  it("clears the armed revert timer on unmount", async () => {
+  it("clears the armed revert timer on unmount", async () => {
     const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
     vi.useFakeTimers({ shouldAdvanceTime: true });
-    const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
 
     const { unmount } = render(<CopyButton text="test" />);
     await user.click(screen.getByTitle("Copy to clipboard"));
     expect(screen.getByTestId("check-icon")).toBeInTheDocument();
 
     // The click armed the 2s revert timer — it is pending mid-window.
     act(() => {
       vi.advanceTimersByTime(500);
     });
     expect(vi.getTimerCount()).toBe(1);
 
     // Unmounting before the timer fires must clear it, so nothing is left
     // pending to call `setCopied` on the gone component. This is the
     // load-bearing assertion: it bites without the useEffect cleanup.
     unmount();
     expect(vi.getTimerCount()).toBe(0);
-
-    // Secondary: advancing past the original window logs no error (e.g. a
-    // "state update on unmounted component" warning), belt-and-suspenders.
-    act(() => {
-      vi.advanceTimersByTime(2000);
-    });
-    expect(errorSpy).not.toHaveBeenCalled();
 
     vi.useRealTimers();
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/components/copy-button.test.tsx` around lines 103 - 132, Remove the
ineffective console.error spy and the subsequent timer-advancement/error
assertion from the “clears the armed revert timer on unmount” test. Keep the
vi.getTimerCount() assertion after unmount as the sole verification of timer
cleanup, and retain necessary fake-timer cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@__tests__/components/copy-button.test.tsx`:
- Around line 103-132: Remove the ineffective console.error spy and the
subsequent timer-advancement/error assertion from the “clears the armed revert
timer on unmount” test. Keep the vi.getTimerCount() assertion after unmount as
the sole verification of timer cleanup, and retain necessary fake-timer cleanup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c08316a4-2d18-43ce-b809-6b3fd8e642cd

📥 Commits

Reviewing files that changed from the base of the PR and between 7c04e77 and 72f3451.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • __tests__/components/copy-button.test.tsx
  • app/components/copy-button.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • CHANGELOG.md
  • app/components/copy-button.tsx

@Nitjsefnie

Copy link
Copy Markdown
Contributor Author

On the CI not running — I think I found why. The CI and Supply Chain workflow runs on the rebased head (72f3451) landed as action_required (event pull_request), i.e. they're sitting behind the fork-PR "approve and run workflows" gate rather than actually executing. That matches the earlier heads too — every recent pull_request run on this branch is action_required, and the one older head that shows success (c472d028, Jul 16) is the one that got approved. So the required quality / test / build / test-e2e jobs aren't failing — they haven't been allowed to start.

Only the app checks that don't need approval (CodeRabbit, Socket) ran, which is why the rollup looks green but mergeable is still blocked on the required contexts.

So a push alone won't kick them off from a fork — it needs a maintainer "Approve and run workflows" on the run (PR checks tab, or the Actions tab). If you approve the run on 72f3451, the required jobs should finally execute; I've verified them locally (lint 0 errors, tsc clean, build green, test:run 2236 passed) so they should come back green.

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Automated code review started - full review. Results will be posted here.

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Re-review in progress — Phase 0-3 complete. All previously reported issues resolved, all review threads closed. Running build & full test suite now...

@hermes-exosphere

Copy link
Copy Markdown
Contributor

🔍 Automated Code Review (Re-review)

📋 Executive Summary

This PR fixes two real-timer bugs in the CopyButton component: rapid re-click timer corruption (stale timer from first click fires early) and unmount-after-copy state-update warnings. The fix stores the timeout ID in a useRef, clears it on re-arm and unmount via a useEffect cleanup, and adds type="button". The code is clean, the tests are correctly structured, and all previously reported issues are resolved. Ready to merge.


📊 Change Architecture

graph TD
    A["CopyButton (before)"] -->|"Bug: no timer tracking"| B["setTimeout leaks on re-click"]
    A -->|"Bug: no unmount cleanup"| C["setCopied fires after unmount"]
    D["CopyButton (after)"] -->|"useRef stores timer ID"| E["clearTimeout before re-arming"]
    D -->|"useEffect cleanup"| F["clearTimeout on unmount"]
    D -->|"Bonus: type=button"| G["No implicit form submit"]
    E --> H["Correct 2s revert window"]
    F --> I["No state-update warning"]
    H --> J["Test: rapid re-click scenario"]
    I --> K["Test: unmount mid-window"]
    style E fill:#90EE90
    style F fill:#90EE90
    style G fill:#90EE90
    style J fill:#90EE90
    style K fill:#90EE90
    style B fill:#FF6B6B
    style C fill:#FF6B6B
Loading

Legend: 🟢 New/Added | 🔴 Removed/Buggy (before)


🔴 Breaking Changes

✅ No breaking changes detected. Component API unchanged (same text, className props, same 2s revert window, same icon behavior).


⚠️ Issues Found

No new issues found. All 3 previously reported issues are resolved:

  1. type="button" added (line 69) — resolves hermes-exosphere's thread
  2. ✅ Unmount test now asserts vi.getTimerCount() to actually verify cleanup — resolves coderabbitai's test-quality concern
  3. useRef<ReturnType<typeof setTimeout>>(undefined) matches the existing pattern in raw-log-viewer.tsx:210 — the same form the PR description cites as the precedent

🔬 Logical / Bug Analysis

Timer lifecycle — verified correct:

  • armRevertTimer() (line 38-44): clears any existing timer before setting a new one → no stale timer can fire
  • useEffect cleanup (line 32-36): clears the ref on unmount → no setCopied on a gone component
  • Only called on successful copy (lines 53, 59) — error paths don't touch the timer
  • handleCopy deps [text, armRevertTimer]armRevertTimer is stable (empty deps), so handleCopy only recreates when text changes

Fallback copy path — verified:

  • Error handler's fallback armRevertTimer() call (line 59) correctly mirrors the success-path call (line 53)
  • Inner catch correctly does nothing when both methods fail

Test quality — verified:

  • Rapid re-click test: clicks at t=0 and t=1.5s, asserts checkmark survives past t=2.0s (stale timer point), then reverts at t=3.5s — confirms the window resets on each click
  • Unmount test: asserts vi.getTimerCount() drops from 1→0 after unmount (load-bearing), plus console.error not-called (belt-and-suspenders) — correctly biting without the cleanup

🧪 Evidence — Build & Test Results

Build Output
[prune-standalone] 2349 files / 46.71 MB -> 1529 files / 33.27 MB (saved 820 files / 13.44 MB)

✅ Build succeeded, TypeScript passes, all routes generate.

Test Results — copy-button tests (8/8 passed)
 ✓ CopyButton > renders copy icon by default
 ✓ CopyButton > copies text to clipboard on click and shows check icon
 ✓ CopyButton > reverts to copy icon after 2 seconds
 ✓ CopyButton > keeps the checkmark for the full 2s window after the last of two rapid clicks
 ✓ CopyButton > clears the armed revert timer on unmount
 ✓ CopyButton > applies custom className
 ✓ CopyButton > falls back to execCommand when navigator.clipboard is unavailable
 ✓ CopyButton > does not show check icon when both copy methods fail

 Test Files  1 passed (1)
      Tests  8 passed (8)
Full Test Suite — 2236/2236 PASSED
 Test Files  126 passed (126)
      Tests  2236 passed (2236)
   Duration  62.32s
Lint — 0 errors (5 pre-existing warnings, none touched by this PR)
✖ 5 problems (0 errors, 5 warnings)

All warnings are pre-existing <img> warnings in unrelated files + one unused eslint-disable directive.


🔗 Issue Linkage

This PR addresses #523 — "Dashboard CopyButton: the 'copied ✓' timer is never cleared". All three items from the issue body are addressed:

  1. ✅ Rapid re-click → timer cleared before re-arming (useRef pattern)
  2. ✅ Unmount mid-window → timer cleared in useEffect cleanup
  3. ✅ Bonus: type="button" added

👥 Human Review Feedback

  • @chhhee10 (Jul 19): Walked through timer paths, confirmed fix is correct, asked for CHANGELOG entry → ✅ CHANGELOG entry added (4559dbe)
  • @chhhee10 (Jul 20, after rebase): Confirmed bunx tsc --noEmit, bun run lint, bun run build — all clean, 2235 passed (now 2236 with the updated test)
  • @Nitjsefnie: Addressed both CodeRabbit concerns empirically, rebased multiple times to keep CHANGELOG clean

💡 Suggestions

  • Consistency — | undefined vs | null: The codebase has mixed patterns for useRef<ReturnType<typeof setTimeout>> initialisation: raw-log-viewer.tsx:210 and this PR use (undefined) without union, while session-hooks-panel.tsx:204 and hooks-client.tsx:416 use | null with null. This PR correctly matches the precedent it cites (raw-log-viewer.tsx). No action needed, but standardising across the codebase would reduce future confusion.

Previous Review Threads (All Resolved)

Thread ID Author Issue Status
PRRT_kwDOR6tUec6Rblc2 hermes-exosphere Missing type="button" ✅ Resolved (line 69)
PRRT_kwDOR6tUec6RuBmN coderabbitai False-positive unmount test ✅ Resolved (vi.getTimerCount() assert)
PRRT_kwDOR6tUec6RuBmn coderabbitai TypeScript useRef type ✅ Resolved (matches raw-log-viewer.tsx precedent)

🏆 Verdict

VERDICT: APPROVED


Automated code review · 2026-07-20 14:00 UTC

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: Approved. ✅

All 2236 tests pass, build succeeds, lint is clean (0 errors). All 3 previously reported issues resolved. Code follows the established useRef pattern. Fixes both defects in #523 correctly.

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: Approved. ✅

@chhhee10 chhhee10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — the rebase is clean and I re-verified it after the push.

Checked the rebase specifically, since that's what changed: it merges into main with no conflict, and app/components/copy-button.tsx and __tests__/components/copy-button.test.tsx are byte-identical to the pre-rebase tip (7c04e77), so only CHANGELOG.md moved. Your #570-alongside-#528 resolution is right. Full gate green locally too — tsc clean, lint 0 errors, build clean, 2236 passed.

Your read on the CI was correct and mine was wrong: a push from a fork doesn't kick the jobs off, they were landing as action_required behind the "approve and run workflows" gate. That's now been approved and quality / test / build / test-e2e all came back green on 72f3451. Thanks for digging into the run conclusions rather than just retrying — that saved a round trip.

For the record on the two earlier bot comments, since both are now resolved but were wrong rather than fixed: type="button" was already present at line 69 when they were filed, and the useRef<ReturnType<typeof setTimeout>>(undefined) "critical TS error" isn't one — React 19.2.4 has the initialValue: T | undefined overload, and tsc --noEmit is clean on your exact head. Nothing for you to action; just so it doesn't look like you quietly changed something under review pressure.

The unmount test asserting getTimerCount() 1 → 0 is the right call — it fails without the useEffect cleanup, which the console.error version wouldn't have.

@NiveditJain
NiveditJain merged commit f81d0be into FailproofAI:main Jul 20, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard CopyButton: the "copied ✓" timer is never cleared

4 participants